package mymyoux.ai
{
	import flash.net.registerClassAlias;
	import flash.utils.IDataInput;
	import flash.utils.IDataOutput;
	import flash.utils.IExternalizable;
	
	import mymyoux.ia.NeuralNetwork;
	import mymyoux.signals.Signal;
	import mymyoux.utils.Maths;

	/**
	 * Free NeuranNetwork
	 */
	public class Brain implements IExternalizable
	{
		/**
		 * @private
		 */
		private static const ALIAS:* = registerClassAlias( "mymyoux.ai.Brain", Brain );
		/**
		 * List of neurons including inputs and outputs
		 */
		protected var _neurons:Vector.<Neuron>;
		/**
		 * List of input neurons
		 */
		protected var _inputs:Vector.<Neuron>;
		/**
		 * List of output neurons
		 */
		protected var _outputs:Vector.<Neuron>;
		/**
		 * @private
		 */
		private var _signalResults:Signal;
		/**
		 * @private
		 */
		private var _signalArchitecture:Signal;
		/**
		 * @private
		 */
		//indique s'il faut rechecker les neurons
		private var _unconnectedNeurons:Boolean;
		/**
		 * @private
		 */
		private var _results:Vector.<Number>;
		/**
		 * Create a Brain object
		 * @param inputs Number of output neurons
		 * @param outputs Number of input neurons
		 * @param neurons Number of neurons
		 */
		public function Brain(inputs:uint = 0, outputs:uint = 0, neurons:uint = 0)
		{
			_unconnectedNeurons = false;
			_neurons = new Vector.<Neuron>();
			_inputs = new Vector.<Neuron>();
			_outputs = new Vector.<Neuron>();
			_results = new Vector.<Number>();
			_signalResults = new Signal();
			_signalArchitecture = new Signal();
			this.inputs = inputs;
			this.outputs = outputs;
		}
		/**
		 * Get List of neurons including inputs and outputs
		 */
		public function getNeurons():Vector.<Neuron>
		{
			return _neurons;
		}
		/**
		 * Get List of input neurons
		 */
		public function getInputs():Vector.<Neuron>
		{
			return _inputs;
		}
		/**
		 * Get List of output neurons
		 */
		public function getOutputs():Vector.<Neuron>
		{
			return _outputs;
		}
		/**
		 * Dispatches when a chance inside the structure is made dispatch(void);
		 */
		public function get signalArchitecture():Signal
		{
			return _signalArchitecture;
		}
		/**
		 * Number of output neurons
		 */
		public function get outputs():uint
		{
			return _outputs.length;
		}

		public function set outputs(value:uint):void
		{
			var i:uint = _outputs.length;
			for(i; i < value; i++)
			{
				createOutput();
			}
			_signalArchitecture.dispatch();
		}
		/**
		 * Number of input neurons
		 */
		public function get inputs():uint
		{
			return _inputs.length;
		}
		/**
		 * Number of neurons including output and input neurons
		 */
		public function get neurons():uint	
		{
			return _neurons.length;
		}
		public function set neurons(value:uint):void
		{
			var i:uint = _neurons.length;
			for(i; i < value; i++)
			{
				createNeuron();
			}
			_signalArchitecture.dispatch();
		}
		/**
		 * Number of neurons without output or input neurons
		 */
		public function get neuronsInterns():uint
		{
			return _neurons.length - _inputs.length - _outputs.length;
		}
		public function set neuronsInterns(value:uint):void
		{
			var i:uint = _neurons.length - _inputs.length - _outputs.length;
			for(i; i < value; i++)
			{
				createNeuron();
			}
			_signalArchitecture.dispatch();
		}
		public function set inputs(value:uint):void
		{
			var i:uint = _inputs.length;
			for(i; i < value; i++)
			{
				
				createInput();
			}
			_signalArchitecture.dispatch();
		}
		/**
		 * Create a entire neuralnetwork
		 * @param inputs Number of input neurons
		 * @param outputs Number of output neurons
		 * @param neurons Number of neurons
		 */
		public function create(inputs:uint = 1, outputs:uint = 1, neurons:uint = 1):void
		{
			this.neurons = neurons;
			this.inputs = inputs;
			this.outputs = outputs;
		}
		/**
		 * Create a neuron
		 * @param inputs Number of input links, 1 or more
		 * @param outputs Number of output links, 1 or more
		 * @return The Neuron
		 */
		public function createNeuron(inputs:uint = 0, outputs:uint = 0):Neuron
		{
			var numInputs:uint = 0;
			var numOutputs:uint = 0;
			var i:uint = 0;
			var neuron:Neuron;
			if(_unconnectedNeurons)
			{
				
				for(i=0; i< _outputs.length ;i++)
				{
					if(_outputs[i].inputs == 0)
					{
						neuron = _outputs[i];
						numOutputs++;
						_outputs.splice(i, 1);
						_neurons.splice(_neurons.indexOf(neuron), 1);						
					}
				}
				for(i=0; i< _inputs.length ;i++)
				{
					if(_inputs[i].outputs == 0)
					{
						neuron = _inputs[i];
						numInputs++;
						_inputs.splice(i, 1);
						_neurons.splice(_neurons.indexOf(neuron), 1);						
					}
				}
			}
			neuron = new Neuron();
			//si un seul neurone on le crée juste
			if(_neurons.length > 0)
			{
				//Si c'est le deuxieme on le relit juste au premier
				if(_neurons.length > 1)
				{
					//TODO:equiprobable ou non
					if(inputs == 0) // on crée de 1 à tous les neurones d'entrée
					{
						inputs = Maths.randBetween(1,_neurons.length - _outputs.length) ;
					}
					if(outputs == 0)// on crée de 1 à tous les neurones de sortie
					{
						outputs = Maths.randBetween(1,_neurons.length - _inputs.length) ;
					}
					//TODO:reflechir sur interdire une double liaison
					var id:uint;
					//on crée des liens vers le nouveau neurone (on exclue donc les liaisons à partir des neurones d'output)
					for(inputs; inputs > 0; inputs--)
					{
						do
						{
							id = Maths.randBetween(1,_neurons.length) - 1;	
						}while(_outputs.indexOf(_neurons[id])>-1)
						neuron.register(_neurons[id]);
					}
					//on crée des liens à partir du nouveau neurone (on exclue donc les liaisons vers des neurones d'input)
					for(outputs; outputs > 0; outputs--)
					{
						do
						{
							id = Maths.randBetween(1,_neurons.length) - 1;
						}while(_inputs.indexOf(_neurons[id])>-1)
			
						_neurons[id].register(neuron);
					}
				}else
				{
					neuron.register(_neurons[0]);
				}
			}
			_neurons.push(neuron);
			if(_unconnectedNeurons)
			{
				_unconnectedNeurons = false;
				for(i=0; i< numInputs ;i++)
				{
					createInput();
				}
				for(i=0; i< numOutputs ;i++)
				{
					createOutput();
				}
			}
			return neuron;
		}
		/**
		 * Create an input neuron and its linked nerve. An input neuron can't be linked to an output neuron or another input neuron. 
		 * @param outputs Number of outputs links. 1 or more
		 * @return The Nerve
		 */
		public function createInput(outputs:uint = 0):Nerve
		{
			var nerve:Nerve = new Nerve();
			var neuron:Neuron = new Neuron();
			var id:uint;
			//nerve linking
			neuron.register(nerve);
			if(_neurons.length > 0)
			{
				
				if(outputs == 0)// on crée de 1 à tous les neurones de sortie
				{
					outputs = Maths.randBetween(1,_neurons.length - _inputs.length - _outputs.length) ;
				}
				_inputs.push(neuron);
				if(_neurons.length >= _inputs.length)
				{
				//linking
					for(outputs; outputs > 0; outputs--)
					{
						
						do
						{
							id = Maths.randBetween(1,_neurons.length) - 1;
							//not linking other inputs and outputs (except if there isn't any neuron)
						}while(_inputs.indexOf(_neurons[id])>-1  || (_outputs.indexOf(_neurons[id])>-1 && _neurons.length - _outputs.length - _inputs.length > -1))
						
						_neurons[id].register(neuron);
					}
				}else
				{
					_unconnectedNeurons = true;
				}
			}else
			{
				_inputs.push(neuron);
			}
			_neurons.push(neuron);
			return nerve;
		}
		/**
		 * Create an output neuron and its linked nerve. An output neuron can't be linked to an input neuron or another output neuron. 
		 * @param inputs Number of inputs links. 1 or more
		 * @return The Nerve
		 */
		public function createOutput(inputs:uint = 0):Nerve
		{
			var nerve:Nerve = new Nerve();
			var neuron:Neuron = new Neuron();
			neuron.signalResult.add(onResult, neuron);
			var id:uint;
			//nerve linking
			nerve.register(neuron);
			if(_neurons.length > 0)
			{
				if(inputs == 0) // on crée de 1 à tous les neurones d'entrée
				{
					inputs = Maths.randBetween(1,_neurons.length - _outputs.length - _inputs.length) ;
				}
				_outputs.push(neuron);
				if(_neurons.length >= _outputs.length)
				{
					//linking
					for(inputs; inputs > 0; inputs--)
					{
						do
						{
							id = Maths.randBetween(1,_neurons.length) - 1;
							//not linking other outputs and inputs (except if there isn't any neuron)
						}while(_outputs.indexOf(_neurons[id])>-1 || (_inputs.indexOf(_neurons[id])>-1 && _neurons.length - _outputs.length - _inputs.length > -1))
							neuron.register(_neurons[id]);
					}
				}else
				{
					_unconnectedNeurons = true;
				}
			}else
			{
				_outputs.push(neuron);
			}
			_neurons.push(neuron);
			return nerve;
		}
		/**
		 * Dispatches when a result is thrown dispatch(Number, [Number...]);
		 */
		public function get signalResults():Signal
		{
			return _signalResults;
		}
		/**
		 * Connects to an external neuron.
		 * It will make a map of this external neuralnetwork
		 * @param neuron Neuron of the external network. (hint: gives all outputs neurons of a network to insert it)
		 * @parma isOutput Indicates if the neuron given is an output neuron
		 */
		public function connect(neuron:Neuron, isOutput:Boolean = true):void
		{
			if(neuron == null)
			{
				return;
			}
			if(neuron is Nerve)
			{
				if(neuron.getLinkedNeurons().length > 0)
				{
					neuron = neuron.getLinkedNeurons()[0];
				}else
				{
					return;
				}
			}
			if(neuron == null)
			{
				return;
			}
			if(_neurons.indexOf(neuron) == -1)
			{
				_neurons.push(neuron);
				
				if(neuron.isInput)
				{
					_inputs.push(neuron);
				}else
				{
					if(isOutput)
					{
						_outputs.push(neuron);
					}
					var p:String;
					var neurons:Vector.<Neuron> = neuron.getLinkedNeurons() 
					for(p in neurons)
					{
						connect(neurons[p], false);
					}
				}
			}
			addListeners();
		}
		/**
		 * Gives values to the network
		 * @param ...values Values given
		 */ 
		public function pushValues(...values):void
		{
			var p:String;
			var p2:String;
			var nerves:Vector.<Neuron>;
			if(values.length > 0)
			{
				for(p in _inputs)
				{
					nerves = _inputs[p].getLinkedNeurons();
					for(p2 in nerves)
					{
						
						if(nerves[p2] is Nerve)
						{
							(nerves[p2] as Nerve).value = values.shift();
							if(values.length == 0)
							{
								break;
							}
						}	
					}					
				}
			}
		}
		/**
		 * Forces discovery of the inserted network
		 */
		public function networkDiscovery():void
		{
			var p:String;
			var p2:String;
			var n:Vector.<Neuron>;
			
			for(p in _neurons)
			{
				n = _neurons[p].getLinkedNeurons();
				for(p2 in n)
				{
					if(n[p2] != null && _neurons.indexOf(n[p2]) == -1)
					{
						_neurons.push(n[p2]);
					}
				}
			}	
		}
		/**
		 * Create an serializable object representing the brain
		 * @param Serialized brain
		 */
		public function serialize():Object
		{
			networkDiscovery();
			var p:String;
			var p2:String;
			var n:Vector.<Neuron>;
			var serialized:Object = new Object();
			var links:Array = new Array();
			var neurs:Vector.<Neuron> = new Vector.<Neuron>();
			
			for(p in _neurons)
			{
				if(!(_neurons[p] is Nerve))
				{
					neurs.push(_neurons[p]);
				}
				//neurs.push(neurons[p].weights);
			}
			
			for(p in neurs)
			{
				 n = neurs[p].getLinkedNeurons();
				 for(p2 in n)
				 {
					if(n[p2] != null && !(n[p2] is Nerve))
					{
						links.push([neurs.indexOf(neurs[p]), neurs.indexOf(n[p2])]);
					}
				 }
				 
			}
			serialized.links = links;
			
			serialized.neurons = neurs;//neurs;
			serialized.outputs = outputs;
			serialized.inputs = inputs;
			return serialized;
		}
		/**
		 * @private
		 */
		//write when recharging the brain
		public function writeExternal( output:IDataOutput ):void
		{
			output.writeObject(serialize());
		}
		/**
		 * @private
		 */
		//read when recharging the brain
		public function readExternal( input:IDataInput ):void
		{
			//neurons loading
			var brain:Object = input.readObject();
			_neurons = brain.neurons;
			var numNeurons:uint = brain.neurons;
			var p:String;
			var i:int;
			for(i = brain.inputs + brain.outputs; i > 0 ; i--)
			{
				_neurons.push(new Nerve());
			}
			for(p in brain.links)
			{
				_neurons[brain.links[p][0]].register(_neurons[brain.links[p][1]]);	
			}	
			
			//nerves creation
			var count:Vector.<uint> = new Vector.<uint>(brain.neurons.length, true);
			for(p in brain.links)
			{
				count[brain.links[p][1]]++;
			}
			for(p in _neurons)
			{
				if(_neurons[p].inputs == 0 && (!(_neurons[p] is Nerve)))
				{
					for(i = 0; i<brain.inputs; i++)
					{
						_neurons[p].register(_neurons[numNeurons+i]);
					}
				}
			}
			count = new Vector.<uint>(brain.neurons.length, true);
			for(p in brain.links)
			{
				count[brain.links[p][0]]++;
			}
			for(p in count)
			{
				if(count[p] == 0 && (!(_neurons[p] is Nerve)))
				{
					for(i = 0; i<brain.outputs; i++)
					{
						_neurons[p].register(_neurons[numNeurons+i+brain.inputs]);
					}
				}
			}
		}
		/**
		 * Return a chain representing the brain.
		 * @return chain
		 */
		public function toString():String
		{
			return "[Brain {"+_neurons+"}]";	
		}
		private function onResult(value:Number, neuron:Neuron):void
		{
			trace(neuron+":"+value);
			while(_results.length < _outputs.length)
			{
				_results.push(undefined);
			}
			var id:int = _outputs.indexOf(neuron);
			if(id > -1)
			{
				_results[id] = value;
				for(id=0; id<_results.length; id++)
				{
					if(isNaN(_results[id]))
					{
						return;
					}
				}
				_signalResults.dispatch.apply(this, mymyoux.utils.Arrays.vectorToArray(_results));
				_results = new Vector.<Number>(_results.length);
				for(id=0; id<_results.length; id++)
				{
					_results[id] = undefined;
				} 
			}
		}
		protected function remListeners():void
		{
			var p:String;
			for(p in _outputs)
			{
				_outputs[p].signalResult.remove(onResult);	
			}
		}
		protected function addListeners():void
		{
			var p:String;
			for(p in _outputs)
			{
				_outputs[p].signalResult.remove(onResult);	
				_outputs[p].signalResult.add(onResult, _outputs[p]);	
			}
		}
	}
}